/**
 * File:   Node.java
 * Author: Gvennet
 */
package dht.node;

import dht.identifier.identifier.Identifier;
import dht.network.Network;
import dht.network.NetworkListener;

import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * todo write javadoc
 */
public final class Node implements NetworkListener
{

	private Network network;

	private DHTNodeModel nodeModel;

	private DHTDataModel dataModel;

	private DHTDataModel getDataModel;

	private HashMap<Identifier, List<DHTNodeContacts>> findingValues;

	public Node(Network network, DHTNodeModel nodeModel, DHTDataModel dataModel, DHTDataModel getDataModel)
	{
		this.network = network;
		this.nodeModel = nodeModel;
		this.dataModel = dataModel;
		this.getDataModel = getDataModel;
		this.findingValues = new HashMap<Identifier, List<DHTNodeContacts>>();
	}

	//@Override

	public void receivePingRequest(InetAddress fromAddress, int fromPort, Identifier id, int messageId)
			throws IOException
	{
		network.sendPingResponse(fromAddress, fromPort, messageId);
		if (!nodeModel.contains(id))
		{
			relocation(id, fromAddress, fromPort);	  //todo new
		}
		if (nodeModel.update(id, fromAddress, fromPort))
		{
			network.sendGetNodesRequest(fromAddress, fromPort, network.getLocalId());
		}

	}

	//@Override

	public void receivePingResponse(InetAddress fromAddress, int fromPort, Identifier id) throws IOException
	{
		if (!nodeModel.contains(id))
		{
			relocation(id, fromAddress, fromPort);	  //todo new
		}
		if (nodeModel.update(id, fromAddress, fromPort))
		{
			network.sendGetNodesRequest(fromAddress, fromPort, network.getLocalId());
		}
	}

	//@Override

	public void receiveGetNodesRequest(InetAddress fromAddress, int fromPort, Identifier id, int messageId, Identifier searchingIdentifier)
			throws IOException
	{
		List<DHTNodeRow> list = nodeModel.get(searchingIdentifier);
		network.sendGetNodesResponse(fromAddress, fromPort, messageId, list);
		if (!nodeModel.contains(id))
		{
			relocation(id, fromAddress, fromPort);	  //todo new
		}
		if (nodeModel.update(id, fromAddress, fromPort))
		{
			network.sendGetNodesRequest(fromAddress, fromPort, network.getLocalId());
		}
	}

	//@Override

	public void receiveGetNodesResponse(InetAddress fromAddress, int fromPort, Identifier id, List<DHTNodeContacts> list)
			throws IOException
	{
		if (!nodeModel.contains(id))
		{
			relocation(id, fromAddress, fromPort);	  //todo new
		}
//посылка GetNodesRequest не имеет смысла, потому что ответ скорей всего не будет отличаться от обрабатываемого в данный момент
		for (DHTNodeContacts aList : list)
		{
			network.sendPingRequest(aList.getInetAddress(), aList.getPort());
		}
	}

	//@Override

	public void receivePutValueRequest(InetAddress fromAddress, int fromPort, Identifier id, int messageId, Identifier valueId, String value)
			throws IOException
	{

		network.sendPutValueResponse(fromAddress, fromPort, messageId);
		//todo сделано, что если узел видит более подходящего кандидата
		DHTNodeRow nearestNode = nodeModel.get(valueId).get(0);
		//todo для хранения значения, то не хранит у себя, а пересылает ему
		if (nearestNode.getId().compareTo(network.getLocalId()) == 0)
		{
			dataModel.put(new DHTDataRow(valueId, value, System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(dataModel.getStorageTime())));
		}
		else
		{
			try
			{
				network.sendPutValueRequest(nearestNode.getInetAddress(), nearestNode.getPort(), valueId, value);
			}
			catch (IOException e1)
			{
				//do nothing
			}
		}
		if (!nodeModel.contains(id))
		{
			relocation(id, fromAddress, fromPort);	  //todo new
		}
		if (nodeModel.update(id, fromAddress, fromPort))
		{
			network.sendGetNodesRequest(fromAddress, fromPort, network.getLocalId());
		}
	}

	//@Override

	public void receivePutValueResponse(InetAddress fromAddress, int fromPort, Identifier id) throws IOException
	{
		if (!nodeModel.contains(id))
		{
			relocation(id, fromAddress, fromPort);	  //todo new
		}
		if (nodeModel.update(id, fromAddress, fromPort))
		{
			network.sendGetNodesRequest(fromAddress, fromPort, network.getLocalId());
		}
	}

	//@Override

	public void receiveGetValueRequest(InetAddress fromAddress, int fromPort, Identifier id, int messageId, Identifier valueId)
			throws IOException
	{
		List<DHTDataRow> values = dataModel.get(valueId);
		if (values.size() > 0)
		{
			network.sendGetValueResponse(fromAddress, fromPort, messageId, values);
		}
		else
		{
			List<DHTNodeRow> nodes = nodeModel.get(valueId);
			DHTNodeRow result = null;
			for (DHTNodeRow nod : nodes)
			{
				if (nod.getId().compareTo(network.getLocalId()) != 0 && nod.getId().compareTo(id) != 0)
				{
					result = nod;
					break;
				}
			}
			network.sendGetValueResponseNodes(fromAddress, fromPort, messageId, valueId, result);
		}
		if (!nodeModel.contains(id))
		{
			relocation(id, fromAddress, fromPort);	  //todo new
		}
		if (nodeModel.update(id, fromAddress, fromPort))
		{
			network.sendGetNodesRequest(fromAddress, fromPort, network.getLocalId());
		}
	}

	//@Override

	public void receiveGetValueResponse(InetAddress fromAddress, int fromPort, Identifier id, List<DHTDataRow> values)
			throws IOException
	{
		System.out.println("receiveGetValueResponse from " + id + " to " + network.getLocalId().toString());
		for (DHTDataRow value : values)
		{
			System.out.println("Значение " + value.getId() + " найдено за " + findingValues.get(value.getId()).size());
			findingValues.remove(value.getId());
		}
		for (DHTDataRow value : values)
		{
			getDataModel.put(value);
		}
		if (!nodeModel.contains(id))
		{
			relocation(id, fromAddress, fromPort);	  //todo new
		}
		if (nodeModel.update(id, fromAddress, fromPort))
		{
			network.sendGetNodesRequest(fromAddress, fromPort, network.getLocalId());
		}
	}

	//@Override

	public void receiveGetValueResponseNodes(InetAddress fromAddress, int fromPort, Identifier id, Identifier valueId, DHTNodeContacts node)
			throws IOException
	{
		System.out.println("receiveGetValueResponseNodes from " + id + " to " + network.getLocalId().toString());
		if (findingValues.containsKey(valueId))
		{
			boolean label = true;
			for (DHTNodeContacts nod : findingValues.get(valueId))
			{
				if (nod.getInetAddress().equals(node.getInetAddress()) && nod.getPort() == node.getPort())
				{
					label = false;
					if (findingValues.get(valueId).size() < 10)
					{
						if (nodeModel.get(valueId).size() > 1)
						{
							DHTNodeRow secondPath = nodeModel.get(valueId).get(1);
							network.sendGetValueRequest(secondPath.getInetAddress(), secondPath.getPort(), valueId);
							findingValues.get(valueId).add(new DHTNodeContacts(secondPath.getInetAddress(), secondPath.getPort()));
						}
						else
						{
							findingValues.remove(valueId);
						}
					}
					else
					{
						findingValues.remove(valueId);
					}
					break;
				}
			}
			if (label)
			{
				network.sendGetValueRequest(node.getInetAddress(), node.getPort(), valueId);
				findingValues.get(valueId).add(node);
			}
		}
		else
		{
			network.sendGetValueRequest(node.getInetAddress(), node.getPort(), valueId);
			ArrayList<DHTNodeContacts> list = new ArrayList<DHTNodeContacts>();
			list.add(node);
			findingValues.put(valueId, list);
		}
		if (!nodeModel.contains(id))
		{
			relocation(id, fromAddress, fromPort);	  //todo new
		}
		if (nodeModel.update(id, fromAddress, fromPort))
		{
			network.sendGetNodesRequest(fromAddress, fromPort, network.getLocalId());
		}
	}

	//@Override

	public void receiveTimeout(DHTNodeContacts node)
	{
		nodeModel.remove(node);
	}

	private void relocation(Identifier newNodeIdentifier, InetAddress newNodeAddress, int newNodePort)
			throws IOException
	{
		Identifier localId = network.getLocalId();
		List<DHTDataRow> data = dataModel.getData();
		for (DHTDataRow aData : data)
		{
			Identifier dataId = aData.getId();
			if (dataId.getDistance(localId).compareTo(dataId.getDistance(newNodeIdentifier)) > 0)
			{
				network.sendPutValueRequest(newNodeAddress, newNodePort, dataId, aData.getValue());
				aData.setTtl(System.currentTimeMillis() + (aData.getTtl() - System.currentTimeMillis()) / 2);
			}
		}

	}
}
